-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local skynet = require 'skynet'
local signal = require 'mc.signal'
local class = require 'mc.class'
local log = require 'mc.logging'
local org_freedesktop_dbus = require 'sd_bus.org_freedesktop_dbus'
local client = require 'network_adapter.client'
local ncsi_comm = require 'ncsi.ncsi_comm'

local ETH_SERVICE_NAME = 'bmc.kepler.bmc_network'
local ETH_PATH = '/bmc/kepler/Managers/1/EthernetInterfaces'
local ctx = require 'mc.context'

local ctx_new = ctx.new()

local ncsi_info = class(nil, nil, true)

function ncsi_info:ctor(db, bus)
    self.db = db
    self.bus = bus
    self.listen_register_count = 0
    self.sig_changed_slot = nil
    self.sig_mac_addr_changed = signal.new()
    self.sig_vlan_info_changed = signal.new()
    self.sig_eth_intf_add = signal.new()
    self.port_init_flag = false
    self.sig_multi_vlan_added = signal.new()
    self.signal_slots = {}
    self.ncsi_info_changed_q = skynet.queue()
end

function ncsi_info:init()
    local nc_info = self.db:select(self.db.NcsiNCInfo):first()
    nc_info.MultiVlanState = false
    nc_info:save()
end

local function ping(bus, service_name, app_path)
    local err
    for _ = 1, 60 do -- 60: 等待60s服务未启动，认为异常
        err = org_freedesktop_dbus.ping(bus, service_name, app_path)
        if not err then
            return
        end
        skynet.sleep(100)
    end
    error(string.format('ping %s %s failed', service_name, app_path))
end
function ncsi_info:wait_bmc_network_service()
    ping(self.bus, ETH_SERVICE_NAME, ETH_PATH)
end

function ncsi_info:ncsi_handle_eth_intf_added()
    self:ncsi_nc_info_init()

    self.sig_eth_intf_add:emit()
end

local function get_active_port_id(db, active_mgmt_port_id)
    local obj = db:select(db.NcsiNCPortInfo):where({MgmtPortId = active_mgmt_port_id}):first()
    if obj then
        log:notice('ncsi: Find active port(%d).', obj.PortId)
        return obj.PortId, true
    end
    return 0xff, false -- 0xff : 为无效端口号
end

function ncsi_info:on_update(changed_infos)
    skynet.sleep(100)
    log:notice('ncsi: NCSIInfoChangedSignal(%s)', changed_infos)
    log:notice('ncsi: Update NC info from resource tree.')
    local switch = {
        ['Mac'] = function(value)
            self:set_eth_mac_addr(value)
        end,
        ['ActivePort'] = function(value)
            local obj = self.db:select(self.db.NcsiNCInfo):first()
            local old_active_port = obj.ActivePort
            local cur_active_port, active_state = get_active_port_id(self.db, tonumber(value))
            obj.ActivePort = cur_active_port
            obj.ActiveStatus = active_state
            obj:save()
            log:notice('ncsi: Set ncsi active port.(MgmtPortId = %d)', tonumber(value))
            ncsi_comm.get_instance():ncsi_change_active_port(self.db, active_state, old_active_port, cur_active_port)
        end,
        ['Init'] = function (value)
            ncsi_comm.get_instance().sig_relink_ncsi_port:emit()
        end
    }
    for _, changed_info in pairs(changed_infos) do
        -- 收到的数据格式为{Field, Value}
        log:notice('ncsi: NCSIInfoChangedSignal(%s: %s)', changed_info[1], changed_info[2])
        if switch[changed_info[1]] then
            log:notice('ncsi: Update %s property value(%s)', changed_info[1], changed_info[2])
            self.ncsi_info_changed_q(function ()
                return switch[changed_info[1]](changed_info[2])
            end)
        end
    end
end

function ncsi_info:on_update_inner_network(msg, path, obj)
    if not obj.VLANId then
        return
    end
    local vlan_id = obj.VLANId:value()
    log:notice('Added InnerNetwork vlan id (%d)', vlan_id)
    self:set_ncsi_multi_vlan(vlan_id)
end

function ncsi_info:listen()
    if self.listen_register_count > 0 then
        return
    end
    self.listen_register_count = self.listen_register_count + 1
    client:SubscribeEthernetInterfacesNCSIInfoChangedSignal(
        function(_, changed_infos)
            self:on_update(changed_infos)
        end)
    client:OnInnerNetworkInterfacesAdded(function(...)
        self:on_update_inner_network(...)
    end)
    -- 监听内网VLAN变化
    client:OnInnerNetworkPropertiesChanged(function(values, _, _)
        local vlan_id = values.VLANId:value()
        log:notice('Changed InnerNetwork vlan id (%d)', vlan_id)
        self:set_ncsi_multi_vlan(vlan_id)
    end)
    -- 监听外网VLAN变化
    client:OnEthernetInterfacesPropertiesChanged(function(values, _, _)
        local vlan_id = values.VLANId:value()
        log:notice('ncsi: Update VLANId property value(%s)', vlan_id)
        self:set_ncsi_vlan_info(tonumber(vlan_id))
    end)
end

function ncsi_info:ncsi_get_eth_obj()
    local objs = client:GetEthernetInterfacesObjects()
    return objs[ETH_PATH]
end

function ncsi_info:ncsi_get_eth_port_obj(mgmt_port_id)
    local ETH_PORT_PATH = ETH_PATH .. '/' .. mgmt_port_id
    local objs = client:GetMgmtPortObjects()
    return objs[ETH_PORT_PATH]
end

function ncsi_info:ncsi_nc_port_register()
    local nc_info = self.db:select(self.db.NcsiNCInfo):first()
    local eth_obj = self:ncsi_get_eth_obj()
    if eth_obj == nil then
        log:error('ncsi: Get eth interface fail')
        nc_info.Enable = false
        nc_info:save()
        return
    end

    local ncsi_enable_flag = false
    local port_info, ok, rsp
    for id = 1, nc_info.PortNum do
        port_info = self.db:select(self.db.NcsiNCPortInfo):where({PortId = id}):first()
        if port_info.PortAvailable then
            ok, rsp = pcall(eth_obj.AddMgmtPort_PACKED, self, ctx_new, nc_info.SlotId,
                port_info.PortId, port_info.Slikscreen, nc_info.EthId, port_info.Type,
                port_info.MgmtPortId)
            if ok then
                port_info.MgmtPortId = rsp.PortId
                port_info:save()
                log:notice('ncsi: Register NC %s successfully, PortId=%d, MgmtPortId=%d.',
                    port_info.Slikscreen, port_info.PortId, port_info.MgmtPortId)
                ncsi_enable_flag = true
            else
                log:error('register ncsi add port failed, err=%s.', rsp)
            end
        elseif port_info.MgmtPortId ~= 255 and port_info.MgmtPortId ~= 0 then
            log:notice('ncsi: Unregistering NC %s, MgmtPortId=%d.', port_info.Slikscreen,
                    port_info.MgmtPortId)
            ok, rsp =
                pcall(eth_obj.DeleteMgmtPort_PACKED, self, ctx_new, port_info.MgmtPortId)
            if ok then
                port_info.MgmtPortId = 0
                port_info:save()
                log:notice('ncsi: Unregister NC %s successfully. PortId=%d.', port_info.Slikscreen,
                    port_info.PortId)
            else
                log:error('register ncsi delete port failed, err=%s.', rsp)
            end
        end
    end

    nc_info.Enable = ncsi_enable_flag
    nc_info:save()
end

function ncsi_info:ncsi_nc_info_init()
    local eth_obj = self:ncsi_get_eth_obj()
    if eth_obj == nil then
        log:error('ncsi: nc info init fail. Get resource tree object is nil.')
        return
    end

    local nc_info = self.db:select(self.db.NcsiNCInfo):first()
    local nc_info_mac = nc_info.Mac
    -- NCSI口的Mac不能固定依赖EthernetInterfaces对象，有持久化数据则优先使用持久化数据
    if nc_info_mac == nil or nc_info_mac == '' or nc_info_mac == '00:00:00:00:00:00' then
        nc_info.Mac = eth_obj.Mac
    end
    nc_info.VlanState = eth_obj.VLANEnable
    nc_info.VlanId = eth_obj.VLANId
    log:notice('ncsi: NC info init.(Mac = %s, VlanState = %s, VlanId = %s)',
        nc_info.Mac, eth_obj.VLANEnable and 1 or 0, eth_obj.VLANId)

    local active_port = self.db:select(self.db.NcsiNCPortInfo):where({MgmtPortId = eth_obj.PortId})
        :first()
    if active_port then
        nc_info.ActivePort = active_port.PortId
        log:notice('ncsi: Active ncsi %s', active_port.Slikscreen)
    else
        nc_info.ActivePort = 0xff -- 0xff: 当前没有切换到NCSI网口，不需要激活端口
    end
    nc_info:save()
end

function ncsi_info:init_inner_network()
    client:ForeachInnerNetworkObjects(function(obj)
        if obj.VLANId then
            log:notice('Init InnerNetwork vlan id (%d)', obj.VLANId)
            self:set_ncsi_multi_vlan(obj.VLANId)
        end
    end)
end

function ncsi_info:ncsi_info_init()
    self:listen()
    self:ncsi_nc_port_register()
    skynet.sleep(200) -- 200: sleep 2s 等待bmc_network模块处理完register任务
    self:ncsi_nc_info_init()
    self:init_inner_network()
end

function ncsi_info:set_eth_mac_addr(mac_addr)
    local obj = self.db:select(self.db.NcsiNCInfo):first()
    local eth = self:ncsi_get_eth_obj()
    obj.Mac = mac_addr
    if eth and eth.Mac ~= mac_addr then
        log:warn('mac addr is unequal with ethernet interfaces, mac addr: %s eth mac: %s', mac_addr, eth.Mac)
    end

    obj:save()
    log:notice('ncsi: Set eth mac addr(%s).', obj.Mac)
    self.sig_mac_addr_changed:emit(obj.Mac)
end

function ncsi_info:set_ncsi_vlan_info(vlan_id)
    local obj = self.db:select(self.db.NcsiNCInfo):first()
    obj.VlanState = vlan_id ~= 0 and 1 or 0
    obj.VlanId = vlan_id
    obj:save()
    log:notice('ncsi: Set ncsi vlan info.(Vlanstate = %d, VlanId = %d)', (obj.VlanState and 1 or 0),
        obj.VlanId)

    self.sig_vlan_info_changed:emit()
end

function ncsi_info:set_ncsi_multi_vlan(vlan_id)
    if vlan_id == 0 then
        return
    end
    local nc_info = self.db:select(self.db.NcsiNCInfo):first()
    nc_info.MultiVlanState = true
    nc_info:save()
    self.sig_multi_vlan_added:emit(vlan_id)
end

function ncsi_info:ncsi_uptade_link_status(port_id, link_status)
    local port_info = self.db:select(self.db.NcsiNCPortInfo):where({PortId = port_id}):first()
    local eth_port_obj = self:ncsi_get_eth_port_obj(port_info.MgmtPortId)
    if eth_port_obj and eth_port_obj.LinkStatus and eth_port_obj.LinkStatus ~= link_status then
        eth_port_obj.LinkStatus = link_status
        log:notice('[network_adapter] update ncsi link_status=%s portid=%s', link_status, port_id)
    end
    port_info.LinkStatus = link_status
    port_info:save()
end

return ncsi_info
